<?php
/** Core Rho Framework file.
* Should be required once at the very beginning by the controller.
*
* Example widgets.php <code>
* require_once '../rho/core.inc.php';
* // other requires and includes go here
* Rho::init('Widgets');
*
* class Widgets_Controller extends HTML_Controller {
* ...
* }
* </code>
*
* @package Rho
* @author Pete
* @version 0.1
* @license http://www.opensource.org/licenses/apache2.0.php Apache License, Version 2.0.*/

/* Errror reporting on */
ini_set('display_errors', TRUE);
error_reporting(E_ALL & ~E_STRICT);


/** The core class that provides static utility methods.
*
* This class is never instantated.
* @package Rho 
* @final */
final class Rho{

  /** @var string Absoulute path to the parent directory containg the rho and app directories.
  * Has a trailing /.
  * This member should be treated as read-only.*/
  public static $PATH='';
  
  /** @var string Absoulute path to the application directory.
  * Has a trailing /. Should not be used before Rho::init() has been called.
  * This member should be treated as read-only.*/
  public static $APP_PATH='';
    
   // Logging type constants
   
   const LOG_OFF=0;
   const LOG_SYSTEM=1;
   const LOG_EMAIL=2;
   const LOG_FILE=4;
   const LOG_SAPI=8;

  /** @var array Configuration settings, stored as an associative array.
  * The inital starting values will be overridden by init() which loads the config file.*/
  private static $conf=array(
    'base_url'=>'http://localhost/rho/www/',
    'dev_mode'=>FALSE,
    'use_rewrite'=>TRUE,
    'url_suffix'=>'.html',
    'ext'=>'.php',
    'secret'=>'change me', # salt used in HMACs
    'webmaster_email'=>'webmaster@localhost', 
    'command'=>'_cmd',
    'enc'=>'UTF-8', # charset encoding - shouldn't get changed
    'attempt_compression'=>TRUE,
    'autoload_func'=>NULL,
    
    'log.errors'=>4, # defaults to the log file
    'log.critical_errors'=>5, # defaults to log file and system
    'log.alerts'=>4, # defaults to log file
    'log.info'=>4, # defaults to file

    
    'lang.base'=>'en',
    'lang.region'=>'GB',
    'lang.varient'=>'',
    'lang.use_i18n'=>TRUE,
    'lang.base_avail'=>array('en', 'de'),
    'lang.region_avail'=>array('en_GB', 'en_US', 'de_DE'),

    'auth.user'=>FALSE, # may be changed by the auth library
    'auth.max_sess_time'=>32400, # 9 hours
    'auth.cookie_path'=>'/',
    'auth.cooke_https_only'=>FALSE,
    'auth.cookie_name'=>'site_user_idi',

    'db.default.type'=>'mysql',
    'db.default.use_perm'=>FALSE,
    'db.default.server'=>'localhost',
    'db.default.username'=>'root',
    'db.default.password'=>'',
    'db.default.flags'=>0,
    'db.default.dbname'=>'rho',
    'db.default.prefix'=>'',
    'db.default.enc'=>'utf8',
    
    'cache.type'=>'File',
    'cache.default_time'=>600 # default 10 minutes
  );
  
  // Private members

  
  /** @var bool Whether init has been called.*/ 
  private static $_has_init=FALSE;
  
  /** @var Rho_Controller Controller instance.*/
  private static $c=NULL;
  
  private static $_filter=array(
    '@<script[^>]*?>.*?</script>@si',   // Strip out javascript
    '@<[\/\!]*?[^<>]*?>@si',            // Strip out HTML tags
    '@<style[^>]*?>.*?</style>@siU',    // Strip style tags properly
    '@<![\s\S]*?--[ \t\n\r]*>@'         // Strip multi-line comments
  );
  
  /** Prevents instances.*/
  private function __construct(){
  }
  
  /** Initialises and dispatches the controller.
  * Should be called by the file containing the controller class.
  * @param string $class_name The name of the controller class excluding the _Controller suffix.
  * @uses Rho_controller::dispatch() */
  public static function init($class_name){
    if(self::$_has_init){ # shouldn't happen
      Rho_Log::error(1002, 'Rho:init() has already been called');
      return FALSE;    
    }
    // load the config file
    include_once self::$APP_PATH.'conf/core.php';
    // Don't attempt compression if zlib.output_compression is on
    if(self::$conf['attempt_compression'] && ini_get('zlib.output_compression')){
      self::$conf['attempt_compression']=FALSE;
    }
    // hide errors if not in dev mode
    if(! self::$conf['dev_mode'] ){
      ini_set('display_errors', FALSE);
    }    
    // create and dispatch the controller
    $class_name .= '_Controller';
    if(! class_exists($class_name) ){
      throw new Rho_Critical_Exception($class_name.' not defined', 5002);
      return FALSE;
    }
    self::$c=new $class_name();
    self::$_has_init=TRUE; # prevent subsequent calls to init
    self::$c->dispatch(); # and off we go!    
    return TRUE;
  }
  
  /** @internal Used by other Rho core classes. Returns the controller or NULL if not created yet.
  * @access private
  * @return Rho_Controller */
  public static function & _getController(){
    return self::$c;
  }
  
  /** Autoloads class on demand. - set by the bootstapping code to be called by __autoload()
  * If the class is not Rho_Log, an info message will be logged.
  * If a function name is set in autoload_func in the core configration file,
  * that function will be called if this method does not find the class.
  * @access private*/
  public static final function _autoload($class_name){
    include_once self::$PATH.'rho/Rho_Log.class.php'; # we always need this :-)
    if($class_name=='Rho_Log' ){ 
      return; # it's ok to for Rho_log to loaded on demand
    }
    
    Rho_Log::info("DEBUG Class $class_name not defined - attempting autoload...\n".Rho_Log::backtrace(debug_backtrace() ) );
    
    // Search for ClassName.class.php in rho, rho/lib and app/lib
    $dirs=array( self::$PATH.'rho/', self::$PATH.'rho/lib/', self::$APP_PATH.'lib/');
    foreach($dirs as $dir) :
      $file=$dir.$class_name.'class.php';
      if(file_exists($file) ){
        include_once $file;
        if(class_exists($class_name, FALSE) ){ # the false here is important, otherwise it will call __autoload again & again
          Rho_Log::info("DEBUG Class $class_name found in $file");
          return;
        } # end class exist
      } # end file exists
    endforeach;
    
    // ok try the main Rho inc files just in case
    foreach( array('cache.inc.php', 'db.inc.php', 'auth.inc.php') as $inc_file) :
      $file=self::$PATH.'rho/'.$inc_file;
      include_once $file;
      if(class_exists($class_name, FALSE) ){
        Rho_Log::info("DEBUG Class $class_name found in $file");
        return;
      } # end class exist
    endforeach;
    
    // try the user defined function if set and exists
    $user_func=self::$conf['autoload_func'];
    if($user_func!=NULL && function_exists($user_func) ){
      Rho_Log::info("DEBUG Now trying $user_func for $class_name");
      call_user_func($user_func, $class_name);
    }
  }
  
  /** Fetches a configuration value.
  * @param string $key The name of the value to fetch.
  * @param mixed $default A value to return if the configuration value does not exist.
  * @return mixed The setting value.*/   
  public static function conf($key, $default=FALSE){
     if( isset(self::$conf[$key]) ){
       return self::$conf[$key];
     }
     if($default===FALSE) Rho_Log::error(1001, "Config value of $key not found");
     return $default;
  }
  
  /** Sets a configuration setting.
  * @param string $key The key to set.
  * @param mixed &$value The new value.*/
  public static function setConf($key,  $value){
     self::$conf[$key]=& $value;
  }
  
  
  // utility functions  
  
  /** Utility function that checks that an array value is set.
  * @param array &$array The array to look in
  * @param string|int $key The array key.
  * @param mixed $default The value to return if the key is not set.
  * @return mixed The array value of $key, or $default.*/
  public static function ifset(array & $array, $key, $default=FALSE){
    if( isset($array[$key]) ){
      return $array[$key];
    }
    return $default;
  }  
  
  /** Utility function that determines the IP address of the visitor.
  * Will check for HTTP_X_FORWARDED_FOR, then defaults to REMOTE_ADDR.
  * @return string A IP address (v.4 or v.6) or 'Unknown IP' .*/
  public static function getIP(){
    if( isset($_SERVER['HTTP_X_FORWARDED_FOR']) ){
      return $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
    return self::ifset($_SERVER, 'REMOTE_ADDR', 'Unknown IP');
  }
  
  /** Utility function that sends a HTTP status header when there's an error.
  * Will also send 'do not chache' headers.
  * @param int $code The HTTP response code.
  * @param string $text The text, e.g. 'Internal Server Error'.*/
  public static function statusHeader($code, $text){
    if(! headers_sent() ){
      header(self::ifset($_SERVER, 'SERVER_PROTOCOL', 'HTTP/1.1').' '.$code.' '.$text, TRUE, $code);
      header('Status: '.$code.' '.$text);
      header('Cache-Control: no-cache, no-store, must-revalidate, post-check=0, pre-check=0');
      header('Expires: '.gmdate('D, d M Y H:i:s', (time()-86400) ).' GMT');
      header('Pragma: no-cache');
    }
    else{
      Rho::logError(1003, 'HTTP Headers already sent');
    }
  }
  
  /** Filters a value, removing tags, invalid unicode, javascript and comments.
  * Based on code by Denham Coote.
  * @param string $value
  * @return string The filtered value. 
  * @uses utf8check()
  * @author Denham Coote
  * @link http://www.denhamcoote.com/php-howto-sanitize-database-inputs */
  public static function filter($value){
    self::utf8check($value);
    if(empty($value) ){
      return '';
    }
    return preg_replace(self::$_filter, '', strip_tags($value) );
  }
  
  /** Checks that a value is valid utf8.
  * If not, $value is converted to a empty string.
  * Uses Harry Feucks' utf-8 library.*/
  public static function utf8check(& $value){
    if(self::$conf['enc']!='UTF-8') return;
    require_once self::$PATH.'rho/lib/utf8/utils/validation.php';
    if(! utf8_is_valid($value) ){
      $value='';
    }
  }
  
  /** @ignore
  * Wrapper of get_object_vars().
  * @see Rho_Controller::loadView() */
  public static function get_object_vars(& $object){
    return get_object_vars($object);  
  }
  
  /** @ignore
  * Wrapper of method_exists().
  * @see Rho_Controller::route() */
  public static function _method_exists(& $object, $method){
    return method_exists($object, $method);
  }
  
  // basic Timedate functions
  
  /** Returns the current date and time in ISO 8601 format.
  * Does not include the timezone component, as it's always UTC.
  * @return string yyyy-mm-dd hh:mm:ss */
  public static function now(){
    return gmdate('Y-m-d H:i:s');
  }  
  
  /** Returns the current GMT date in ISO 8601 format.
  * @return string yyyy-mm-dd .*/
  public static function nowDate(){
    return gmdate('Y-m-d');
  }
  
   /** Returns the current GMT time in ISO 8601 format.
  * Does not include the timezone component, as it's always UTC.
  * @return string hh:mm:ss */
  public static function nowTime(){
    return gmdate('H:i:s');
  }

  /** @ignore
  * Handles errors by throwing an ErrorException.
  * @link http://www.php.net/manual/en/function.set-exception-handler.php
  * @see exception_handler() */
  public static function exception_error_handler($errno, $errstr, $errfile='Unknown File', $errline=0 ) {
    if(error_reporting()==0) return; # if @ suppression, don't do anything
    if($errno==E_WARNING || $errno==E_USER_ERROR || $errno==E_USER_WARNING){
      throw new ErrorException($errstr, $errno, $errno, $errfile, $errline); # this will be caught be rho::exception_handler
    }
    // Otherwise, minor error
    $msg=$errstr.' in '.$errfile.' line #'.$errline;
    # if in development mode, echo 
    if(self::$conf['dev_mode']){ 
      echo '<pre>';
      echo 'Error #'.$errno.' '.$msg;
      echo "\n".Rho_Log::backtrace(debug_backtrace());
      echo '</pre>';
    }
    // log it
    Rho_Log::error($errno, $msg);
    return TRUE;
  }  
  
  /** @ignore
  * Handles uncaught exceptions.
  * @link http://www.php.net/manual/en/function.set-error-handler.php*/
  public static function exception_handler($e){
    Rho_Log::_exceptionHeaders($e);
    if(self::$c!=NULL){
      try{
          self::$c->_onError($e );
        }
        catch(Exception $e){
          //swallow
        }
    }
    else {
      @include_once Rho::$PATH.'rho/i18n.class.php';
      echo '<pre>';
      i18n::tr('core.error');echo ' #'.$e->getCode();
      if(self::$conf['dev_mode'] ){
        echo "\n".$e->getMessage()."\n".$e->getFile().' line #'.$e->getLine()."\n";
        echo $e->getTraceAsString();
        echo "\n".Rho_Log::backtrace(debug_backtrace());
      }
      echo '</pre>';
    }
    // log it
    Rho_Log::error($e->getCode(), $e->getMessage());
    // if not in dev mode, die
    if(! self::$conf['dev_mode'] ){
      die(2);
    }
  }
}

/* Here is bootstap code. */

// exception & error handling
set_exception_handler('Rho::exception_handler');
set_error_handler('Rho::exception_error_handler');

// set the PATHs
Rho::$PATH=realpath(dirname(dirname(__FILE__))).'/';
Rho::$APP_PATH=Rho::$PATH.'app/';


// multibyte support
if( extension_loaded('mbstring') ){
  mb_internal_encoding('UTF-8');
}
else { # load in the fix
  /** Load the core UTF-8 library functions.*/
  require_once Rho::$PATH.'rho/lib/utf8/utf8.php';
  /** load the fake MB functions */
  require_once Rho::$PATH.'rho/mb_fix.inc.php';
}

// emergency autoloading
/** @ignore */
function __autoload($class_name) {
  Rho::_autoload($class_name);
}

// turn not-so-magic quotes off
// Hmm already depreceated in 5.3 and will be removed in 6
// wonder is the get functions will still exits though? maybe better using ini_get ? 
if(@get_magic_quotes_runtime() ){
  set_magic_quotes_runtime(FALSE);
  Rho_Log::info('FIX Turn off magic_quotes_runtime in php.ini');  
}
if(ini_get('magic_quotes_sybase') ){
  ini_set('magic_quotes_sybase', FALSE); 
} 
if(@get_magic_quotes_gpc() ){ // oh noes !!!
  Rho_Log::info('FIX Turn off magic_quotes_gpc in php.ini');
  /** @ignore */  
  function rho_stripslashes_gpc(&$value) {
    $value = stripslashes($value);
  }
  array_walk_recursive($_GET, 'rho_stripslashes_gpc');
  array_walk_recursive($_POST, 'rho_stripslashes_gpc');
  array_walk_recursive($_COOKIE, 'rho_stripslashes_gpc');
  array_walk_recursive($_REQUEST, 'rho_stripslashes_gpc');
}

/* End bootstrap code */


/* Controller classes and model interface */

/** The base controller class in the MVC pattern.
*
* All controllers must extend from this class, either directly or indirectly.
*
* For (X)HTML pages, extend the Rho_HTML_Controller instead.
*
* Extend this class for other types of content (RSS feeds, XML, etc.)
* @see Rho_HTML_Controller
* @package Rho */
class Rho_Controller{

  /** @var int How long the CLIENT should cache the repsonse in MINUTES.
  * If this value is 0 (or there is a POST submission), then the client will be explicitly told not to cache the repsonse.
  * Client-side caching is always disabled when config setting 'dev_mode' is TRUE.
  * May be changed on an action by action basis.*/
  protected $cache=10;
  
  /** @var int Optional - When the result was last modified (e.g. from a database record timestamp) in UNIX time GMT.
  * Optionaly used for the Last-Modified header when dealing with modified-since requests when caching is enabled.
  * If this is value is set (i.e. not zero) and is greater than the current time less $cache minutes,
  * then this value will be used. May be changed on an action by action basis. */
  protected $last_modified=0;
  
  /** @var string The MIME content type of the controllers response.
  * May vary from controller action to action. */
  protected $content_type='text/html';
  
  /** @var bool Whether we've sent HTTP headers or not.*/  
  protected $headers_sent=FALSE;
  
  /** @var string The file name extension of the view files (without a dot). Defaults to 'php'.*/
  protected $view_ext='php';
  
  /** @param $uri Array of strings after */
  protected $uri=array();
  
  protected $item_id=NULL;
  
  protected $default_item_action='view';
  
  /** @var bool If this controller needs translation functions - default TRUE.*/
  protected $need_i18n=TRUE;
  
  /** @var bool If we have an output buffer (i.e. gz_handler). Used internally.*/
  protected $have_ob=FALSE;
  
  private $_post=array();
  
  private $_get=array();
  
  private $_cookie=array();
    
  /** Constructor */
  public function __construct(){
  
  }
  
  
  /** Filtered access to $_GET .
  * Uses this method instead of $_GET directly.
  * @param string $key The key of the $_GET array to access.
  * @param string|int|float|bool Value to return if $key is not set.
  * @return string The filtered value or $default.
  * @uses Rho::filter()
  * @internal uses $_get
  * @see post(), cookie() */
  public final function get($key, $default=FALSE){
    if(isset($this->_get[$key]) ){
      return $this->_get[$key];
    }
    global $_GET;
    if(isset($_GET[$key]) ){
      $this->_get[$key]=Rho::filter($_GET[$key]);
    }
    else {
      $this->_get[$key]=$default;
    }
    return $this->_get[$key];
  }
  
  /** Filtered access to $_POST .
  * Uses this method instead of $_POST directly.
  * @param string $key The key of the $_POST array to access.
  * @param string|int|float|bool Value to return if $key is not set.
  * @return string|mixed The filtered value or $default.
  * @uses Rho::filter()
  * @internal uses $_post
  * @see get(), cookie() */
  public final function post($key, $default=FALSE){
    if(isset($this->_post[$key]) ){
      return $this->_post[$key];
    }
    global $_POST;
    if(isset($_POST[$key]) ){
      $this->_post[$key]=Rho::filter($_POST[$key]);
    }
    else {
      $this->_post[$key]=$default;
    }
    return $this->_post[$key];
  }
  
  /** Filtered access to $_COOKIE .
  * Uses this method instead of $_COOKIE directly.
  * @param string $key The key of the $_COOKIE array to access.
  * @param string|int|float|bool Value to return if $key is not set.
  * @return string|mixed The filtered value or $default.
  * @uses Rho::filter()
  * @internal uses $_cookie
  * @see post(), get() */
  public final function cookie($key, $default=FALSE){
    if(isset($this->_cookie[$key]) ){
      return $this->_cookie[$key];
    }
    global $_COOKIE;
    if(isset($_COOKIE[$key]) ){
      $this->_cookie[$key]=Rho::filter($_COOKIE[$key]);
    }
    else {
      $this->_cookie[$key]=$default;
    }
    return $this->_cookie[$key];
  }
  
  /** Used by Rho::init() only - DO NOT CALL.
  * This function simply calls route(). 
  * @see  Rho::init()
  * @uses route() */
  public final function dispatch(){
    if($this->need_i18n ){
      /* Load the translation class */
      require_once Rho::$PATH.'rho/i18n.class.php';
    }
    $this->route();
  }
  
  /** Determines what action to take based on the URI and/or client request - DO NOT CALL DIRECTLY.
  * Called by dispatch, which is called by Rho::init().
  * This method should not be called by anywhere else.
  * @see dispatch() */
  protected function route(){
    $action=$this->determineAction();
    if(empty ($action) ){
      $this->index();
    }
    else{
      if(substr($action, 0, 1)=='_' ){
        throw new Rho_Critical_Exception('Forbidden action '.$action, 5410, 410, 'Forbidden');
      }
      if(Rho::_method_exists($this, $action) ){
        $this->$action();
      }
      else {
        echo $action;
        throw new Rho_404();
      }
    }
  }
  
  /** @return string The action name.*/
  protected function determineAction(){
    global $_SERVER;
    $self=explode('/', $_SERVER['PHP_SELF']);
    if(Rho::conf('use_rewrite', TRUE) ){
      $me=basename($_SERVER['PHP_SELF'], Rho::conf('ext', '.php') );
    }
    else{
      $me=basename($_SERVER['SCRIPT_NAME']);
    }
    
    for($i=0; $i<count($self); $i++ ) :
      if($self[$i]=='_dispatch.php'){
        $this->uri=array_slice($self, ($i+2) );
        break;
      }
      if($self[$i]==$me){
        $this->uri=array_slice($self, ($i+1) );
        break;
      }
    endfor;
    if(count($this->uri)>0){
      if( empty($this->uri[0]) ) {
        return '';
      }
      $action=$this->uri[0] ;
      array_shift($this->uri);
      if( (! empty($this->default_item_action)) &&  (! Rho::_method_exists($this, $action) ) ){
        $this->item_id=$action;
        $action= count($this->uri)>0 && !empty($this->uri[0]) ? $this->uri[0] : $this->default_item_action;
      }
      return $action;
    }
    return '';
  }
  
  /** Sends the HTTP headers necessary for the response.
  *
  * Normally this method will be called automatically by the first call to loadView and does not need to be called.
  *
  * This sends the content type and the respective cache control headers.
  * If you're using PHP's sessions, you MUST call this method BEFORE calling session_start().
  *
  * @see loadView()
  * @uses $headers_sent
  * @uses sendNoCacheHeaders() 
  * @uses lastModified()*/
  protected final function sendHeaders(){
    if($this->headers_sent) return;
    global $_SERVER;
    if(! headers_sent() ){
      # MIME type
      header('Content-Type: '.$this->content_type.'; charset='.Rho::conf('enc', 'UTF-8') );
      # attempt gz output compression      
      if(Rho::conf('attempt_compression') ){ 
        $this->have_ob=@ob_gzhandler();
      }
      # now caching
      if( (! Rho::conf('dev_mode')) && (! $_POST) && $this->cache>0 ){
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ){ # revalidation of a cached response
          $this->lastModified($data_last_modified);
        }
        $secs=$this->cache * 60;
        $visibility='public';
        session_cache_limiter('public');
        if(Rho::conf('auth.user')) {
          $visibility='private';
          session_cache_limiter('private_no_expire');
        }
        session_cache_expire($this->cache);
        header("Cache-Control: $visibility, must-revalidate, max-age=$secs");
        header('Expires: '.gmdate('D, d M Y H:i:s', (time() + $secs) ).' GMT');
      }
      else { # no caching
        $this->sendNoCacheHeaders();
      } 
    } # end if headers not sent
    else {
      Rho_Log::error(1003, 'HTTP Headers already sent');
    }
    $this->headers_sent=TRUE;
  }
  
  /** Sends HTTP headers to indicate the response should not be cached by the client.
  *
  * Does NOT do any checking - you should check headers_sent() or $this->headers_sent first.
  * @see sendHeaders() */
  protected final function sendNoCacheHeaders(){
    header('Cache-Control: no-cache, no-store, must-revalidate, post-check=0, pre-check=0');
    header('Expires: '.gmdate('D, d M Y H:i:s', (time()-86400) ).' GMT');
    header('Pragma: no-cache');
    session_cache_limiter('nocache');
  }
  
  
  /** Called by sendHeaders() if client side caching is active AND  HTTP_IF_MODIFIED_SINCE is set in the client request.
  *
  * If HTTP_IF_MODIFIED_SINCE is greater than the modified time, 304 status HTTP headers are sent and the script terminates.
  * @access private
  * @see sendHeaders(), $last_modified */
  protected final function lastModified(){
    global $_SERVER;
    $since=strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    if($since==0) return; 
    $secs=$this->cache * 60;
    $max_age=time()-$secs;
    if($this->last_modified > $max_age){
      $max_age=$this->last_modified;
    }
    if( $since > $max_age){
      Rho::statusHeader(304, 'Not Modified');
      $expires=$since-$max_age;
      $visibility='public';
      if(Rho::conf('auth.user')) $visibility='private';
      header('Last-Modified: '.gmdate('D, d M Y H:i:s', $max_age).' GMT');
      header("Cache-Control: $visibility, must-revalidate, max-age=".$expires );
      header('Expires: '.gmdate('D, d M Y H:i:s', (time()+$expires) ).' GMT');
      exit(0);
    }
  }
  
  /** Loads and outputs a view file.
  *
  * The default implementation uses PHP files for the view.
  *
  * @param string $name The name of the view to load (without a filename extension).
  * @param array $extra Optional associative array of variables to pass to the view.
  * @see getView()
  * @uses $headers_sent  
  * @uses $view_ext 
  * @uses sendHeaders() */
  public function loadView($name, $extra=NULL){
    if(empty($name) ) return;
    if(! $this->headers_sent) $this->sendHeaders();
    // pass all public members of this controller to the view by extracting them to the current scope
    $data=Rho::get_object_vars($this);#we use Rho::get_object_vars so private members aren't passed
    if($extra!=NULL){
      $data=array_merge($data, $extra);
    }
    extract($data, EXTR_SKIP); 
    // now load the view
    include Rho::$APP_PATH.'views/'.$name.'.'.$this->view_ext;
  }
  
  /** Loads a view and returns it as a string.
  *
  * The view will not be sent, unlike loadView()
  *
  * @param string $name The name of the view to load (without a filename extension).
  * @param array $extra Optional associative array of variables to pass to the view.
  * @see loadView()
  * @uses loadView()
  * @return string */
  protected function getView($name, $extra=NULL){
    ob_start();
    $this->loadView($name, $extra); 
    $result=ob_get_contents();
    ob_end_clean();
    return $result;
  }
  
  /** Default controller action.
  * Called by route() when no action has been specified.
  * Should be overidden by child classes - by default it throws a 404 exception.*/
  protected function index(){
    throw new Rho_404();
  }
  
  /** Called when an critical error has occurred and the script will terminate.
  *
  * This is called when: 
  *  - There is an uncaught expception
  *  - There is a major error (in which cause $e will be a ErrorException )
  *  - Rho::log() was called with an instance of Rho_Critical_Exception or it's decendants.
  *
  * The HTTP status code header will already been sent.
  * This method must NOT call exit() or die() - this will be done after it returns when appropiate.
  *
  * The Base implementation outputs the error message.
  * @param Exception &$e The Rho_Critical_Exception (or uncaught exception) that is responsible.
  * @see Rho::log() */
  public function _onError(Exception & $e){
    // make sure we have the translation functions
    include_once Rho::$PATH.'rho/i18n.class.php';
    // send the content headers if not already sent and if we can
    if(! headers_sent() ){
      $this->cache=0;
      $this->sendHeaders();
    }
    // are we sending out html ? If yes we can output HTML, otherwise we output plain text
    $html= $this->content_type=='text/html' ? TRUE : FALSE;
    
    // if in dev_mode, spit out the ugly details...
    if( Rho::conf('dev_mode') ){
      if($html) echo '<pre class="error">';
      i18n::tr('core.error');  echo ' #'.$e->getCode();
      echo "\n".$e->getMessage()."\n".$e->getFile().' line #'.$e->getLine()."\n";
      echo $e->getTraceAsString();
      echo "\n";
      if($html) echo '</pre>';
      return;
    }
    
    // not dev mode, so output something pretty(ier)
    if($html) echo '<h1>';
    // if its a Rho_Exception, we can output the message based on the HTTP code
    if($e instanceof Rho_Exception){
        i18n::tr('core.error_'.$e.getHttpCode() ); # e.g. base.error_404
        $msg=i18n::tr('core.error_'.$e.getHttpCode().'_details', FALSE);
      }
      else {
        i18n::tr('core.error_500');
        $msg=i18n::tr('core.error_500_details' , FALSE);
    }
    
    if($html) echo '</h1>'."\n".'<p class="errror">';
    echo $msg;
    if($html) echo '<br />';
    echo "\n";  i18n::tr('core.error');  echo ' #'.$e->getCode();
    if($html) echo '</p>';
  }
  
}

/** A base controller class for ouputting (X)HTML pages.
*
* Provides several enhancements over the core Rho_Controller.
* @package Rho*/
class Rho_HTML_Controller extends Rho_Controller{

  /** @var string The title of the page, which will appear in the <title> tag.*/
  public $title='Title';
  /** @var string The description of the page, which will appear in the <meta name="description"> tag.*/
  public $description='Description';
  /** @var string Extra content that will apppear inside the <head> tag.*/
  public $head_extra='';
  /** @var string The language of the page - will be set automatically. */
  public $lang='en';
  /** @var string The language direction of the page - either ltr or rtl .*/
  public $lang_dir='ltr';
  /** @var string List of keywords for the page that appear in the <meta name="keywords> tag.
  * Will be set automatically by loadHeader() dependant on the content of keywords.*/
  public $page_keywords='';
  
  /** @var string The name of the header view.*/ 
  protected $header_view='_header';
  /** @var string The name of the footer view.*/
  protected $footer_view='_footer';
  /** @var array List of page keywords.
  * @see setKeywords(), $page_keywords */
  protected $keywords; 
  /** @var bool If the header has been loaded - used internally .*/
  protected $header_loaded=FALSE;
  /** @var bool If the footer has been loaded - used internally .*/
  protected $footer_loaded=FALSE;

  /** Constructor - does nothing.*/
  public function __construct(){
  
  }
  
  /** Loads the header view if it hasn't already been loaded.
  * @see $header_view
  * @uses loadView()
  * @uses $header_loaded */
  protected function loadHeader(){
    if($this->header_loaded) return;
    $this->loadView($this->header_view);
    $this->header_loaded=TRUE;
  }
  
  /** Loads the footer view if it hasn't already been loaded.
  * @see $footer_view
  * @uses loadView()
  * @uses $footer_loaded */
  protected function loadFooter(){
    if($this->footer_loaded) return;
    $this->loadView($this->footer_view);
    $this->footer_loaded=TRUE;
  }
  
  /** Loads and outputs a view file.
  *
  * If the header view hasn't already been loaded, it will load this first.
  * To override this behaviour, set $this->header_loaded to TRUE.*/
  function loadView($name, $extra=NULL){
    if( ! $this->header_loaded && $name!=$this->header_view ){
      $this->loadHeader();
    }
    parent::loadView($name, $extra);
  }
  
  /** Determines what action to take based on the URI and/or client request - DO NOT CALL DIRECTLY.
  * Called by dispatch, which is called by Rho::init().
  * This method should not be called by anywhere else.
  *
  * Calls parent::route(), then after routing has completed, it will check if the footer has been loaded,
  * and if not, will load it.
  * To stop this behaviour, set $footer_loaded to TRUE in the controller's action method.
  * @see Rho_Controller::route(), $footer_loaded*/
  protected function route(){
    parent::route();
    if(! $this->footer_loaded ){
      $this->loadFooter();
    }
  }
}

/** The interface definition for the Models in the MVC pattern.
*
* All model classes should declare that they implement this interface .
* The interface is used purely for identification, and does not define any methods to allow model flexablility.
* @package Rho */
interface Rho_Model{

}

/* Exception classes */

/** Standard framework exception.
* @package Rho
* @subpackage Rho-exceptions
*/
class Rho_Exception extends Exception{

  /** @var int HTTP status code, e.g. 500.*/  
  private $_http_status;
  /** @var string HTTP Status message, e.g. 'Internal Server Error'.*/
  private $_http_text;

  /** Creates a Rho_Exception.
  * @param string $message Error message.
  * @param int code Error code.
  * @param Exception $previous
  * @param int $http_code HTTTP error status code associated for this exception - default 500.
  * @param string $http_text HTTTP status text associated for this exception - e.g. 'Internal Server Error'.*/
  public function __construct ($message='Rho Framework Exception' , $code=1000, Exception $previous=NULL, $http_code=500, $http_text='Internal Server Error'){
    $this->_http_status=$http_code;
    $this->_http_text=$http_text;
    parent::__construct($message, $code /*, $previous*/);
  }
  
  /** Returns the HTTP status code associated with this exception.
  * @return int By default, 500 (internal server error).*/
  public final function getHttpCode(){
    return $this->_http_status;
  }
  
  /** Returns the HTTP status text associated with this exception.
  * @return string By default 'Internal Server Error'.*/
  public final function getHttpText(){
    return $this->_http_text;
  }
  
}

/** Critical Exception.
* This will always prevent further code execution when logged with Rho::log().
*
* @see Rho::log()
* @package Rho
* @subpackage Rho-exceptions
*/
class Rho_Critical_Exception extends Rho_Exception{
  
  public function __construct ($message = 'Critical Rho Framework Exception' ,$code=5000, Exception $previous=NULL, $http_code=500, $http_text='Internal Server Error'){
    parent::__construct($message, $code, $previous, $http_code, $http_text);
  }
  
}

/** A 404 Page Not Found exception.
* This is a critical exception, so calling Rho::log() on an instance of this class will halt the code.
* @see Rho::log(), Rho_Critical_Exception
* @package Rho
* @subpackage Rho-exceptions
*/
class Rho_404 extends Rho_Critical_Exception{

  /** Creates a 404 Exception.
  * @param string $message Error message.
  * @param int code Error code.
  * @param Exception $previous */
  public function __construct($message='Page Not Found', $code=404, Exception $previous=NULL){
    parent::__construct($message, $code, $previous, 404, 'Page Not Found');
  }

}